feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)#52
Conversation
…orktree, stub Agent) Constructs a live *session.Manager in main alongside the LCM, sharing the exact same SessionStore + LCM dependencies the lifecycle stack already holds. Refactor: storeAdapter moves from package main to a new internal package, wiring.Adapter, so the daemon's composition root and any in-process integration tests can share a single bridge. Stubbed for now: ports.Agent has no production adapter on main; a loud *noopAgent returns sentinel AO_AGENT_HARNESS_NOT_WIRED and logs a warning once on first call, so a future Spawn through this lane fails at the runtime layer with a clear breadcrumb rather than starting a broken session quietly. ports.Notifier and ports.AgentMessenger remain stubbed alongside the LCM. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renames the unused context.Context parameter from `_` to `ctx` so the parameter name is already in place when a future plugin constructor needs to honor cancellation (tmux/gitworktree are synchronous today). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Greptile SummaryThis PR wires a live
Confidence Score: 5/5Safe to merge — the shutdown ordering on the new startSession error path is correct, the shared store and LCM pointers are verified by the new test, and all new code paths are guarded by loud failures rather than silent no-ops. The core wiring is a clean move: storeAdapter is lifted verbatim into the new package, the SM is composed over the exact same Adapter and LCM pointer the lifecycle stack holds, and the error path in main.go correctly drains the reaper and CDC pipeline before the deferred store.Close fires. The only fragility is the reflect+unsafe field inspection in the test, which relies on unexported field names staying stable — a compile-time-invisible assumption that would surface as a runtime test failure if session.Manager is ever refactored. backend/wiring_test.go — the reflect+unsafe field inspection in inspectSessionDeps silently assumes the store and lcm field names of session.Manager never change; a rename would produce a runtime test failure rather than a compile error. Important Files Changed
Sequence DiagramsequenceDiagram
participant main
participant startLifecycle
participant startSession
participant wiring.Adapter
participant lifecycle.Manager
participant session.Manager
participant tmux.Runtime
participant gitworktree.Workspace
participant noopAgent
main->>startLifecycle: startLifecycle(ctx, store, log)
startLifecycle->>wiring.Adapter: "Adapter{Store: store}"
startLifecycle->>lifecycle.Manager: lifecycle.New(a, a, noopNotifier, noopMessenger)
startLifecycle-->>main: "lifecycleStack{LCM, Adapter, reaperDone}"
main->>startSession: startSession(ctx, cfg, lcStack, log)
startSession->>tmux.Runtime: "tmux.New(Options{})"
startSession->>gitworktree.Workspace: "gitworktree.New(Options{ManagedRoot, StaticRepoResolver{}})"
startSession->>noopAgent: newNoopAgent(log)
startSession->>session.Manager: "session.New(Deps{Runtime, Agent, Workspace, lcStack.Adapter, noopMessenger, lcStack.LCM})"
note over session.Manager,wiring.Adapter: SM.store == lcStack.Adapter (same *sqlite.Store)
note over session.Manager,lifecycle.Manager: SM.lcm == lcStack.LCM (same pointer)
startSession-->>main: "sessionStack{SM}"
main->>main: srv.Run(ctx)
main->>main: stop() → lcStack.Stop() → cdcPipe.Stop()
Reviews (2): Last reviewed commit: "fix(backend): drain reaper + cdc poller ..." | Re-trigger Greptile |
If startSession returned an error, run() returned immediately and the reaper + cdc poller goroutines kept running while defer store.Close() fired — a data race against the SQLite handle. Mirror the bottom-of-run shutdown sequence on the error path (cancel ctx, drain reaper, drain poller) so both goroutines have exited before the store is closed. The explicit-not-defer ordering is the same the existing post-srv.Run shutdown block uses; piling on more defers would hit the LIFO trap the same comment already warns about. Reported by Greptile on PR #52. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Construct a live
*session.Managerinsidebackend/main.goover realtmux.Runtime, realgitworktree.Workspace, the SQLite-backedSessionStore/PRWriter, and the same*lifecycle.Managerthe LCM already holds.ports.Agentis a loud stub (no production adapter exists onmain);Notifier/AgentMessengerremain stubbed alongside the LCM. No HTTP routes are wired in this PR — that's the daemon lane's territory.The refactor extracts the old
storeAdapterfrompackage maininto a new internal package,internal/storage/sqlite/wiring, so the daemon's composition root and any in-process integration tests can share one canonical bridge instead of duplicating it.File-by-file
backend/internal/storage/sqlite/wiring/adapter.go(NEW, 107 lines)package wiring. ExportsAdapter struct{ *sqlite.Store }, methodsPRFactsForSession+WritePR, helperprState, and bothvar _ ports.SessionStore = Adapter{}/var _ ports.PRWriter = Adapter{}assertions — moved verbatim fromlifecycle_wiring.go:45-128.backend/lifecycle_wiring.gostoreAdapter+ the two var assertions +PRFactsForSession+WritePR+prState(all moved). SwitchesstartLifecycletowiring.Adapter{Store: store}and exposes the adapter onlifecycleStack.Adapter. AddssessionStack,startSession, and the loud-stub*noopAgent(sentinelAO_AGENT_HARNESS_NOT_WIRED,sync.Oncewarning on first call). Reuses existingnoopNotifier/noopMessenger.backend/main.gostartSession(ctx, cfg, lcStack, log)right afterstartLifecycle. Holds the*sessionStackin a local (no HTTP route uses it yet). Rewrites theNOT wired here yetblock at lines 73-86 to reflect the new reality: Runtime + Workspace are real, onlyports.Agentis a loud stub,Notifier/Messengerstubs remain pending their multiplexers, HTTP routes still deferred.backend/wiring_test.goTestWiring_WriteFlowsToBroadcasterto usewiring.Adapter. AddsTestWiring_SessionManagerSharesLifecycleStoreAndLCM: assertsstartSessionreturns non-nil, then verifies (viareflect+unsafescoped to one helper) that the SM'sstorefield is the exact samewiring.Adapterand itslcmfield is the exact same*lifecycle.Managerthe lifecycle stack owns — proving a single canonical pair, not two parallel stacks. The unsafe access is the smallest path that respects the brief's no-modify-session/manager.goconstraint.git diff --stat origin/main...HEAD:NOT in this PR (intentional)
ports.Agentadapter — none exists onmain; the loud stub gives the SM a working Spawn shape that fails clearly when invoked. A per-harness Agent adapter (Claude Code / Codex / Cursor) is the next lane.SessionManager— owned by the daemon lane (Tracking: Go HTTP Daemon — REST #10).httpd.Newkeeps its current signature; SM is held in a local so wiring routes later is a one-line plumb-through.session/aa-31) already covers SM+LCM+SQLite live-fire under its own stub plugins.RepoResolver—startSessionpasses an emptygitworktree.StaticRepoResolver{}. Any future Spawn for any project fails withgitworktree: no repo configured for project %q— the right loud failure until a projects table feeds repo paths in (hard-coding one repo here would silently misroute spawns).noopNotifier/noopMessenger), tracked separately.lifecycle/manager.go,session/manager.go,ports/*,domain/*, or migrations — explicitly forbidden by the brief; verified untouched.Follow-up for PR #49
backend/internal/integration/lifecycle_sqlite_test.goon PR #49's branch (session/aa-31) currently carries its own duplicatestoreAdapter. Once both this PR and #49 land, that duplicate should switch towiring.Adapter. I deliberately did not modify #49's branch (per the brief).Test plan
go build ./...go vet ./...go test -race -count=1 ./...(215 pass across 18 packages — was 214; the +1 is the new wiring test)go test -race -count=1 -run TestWiring ./.(bothTestWiring_WriteFlowsToBroadcasterandTestWiring_SessionManagerSharesLifecycleStoreAndLCMpass under-race)0672dbb(git log -1 origin/main)🤖 Generated with Claude Code